package com.fteams.siftrain.assets;
import com.badlogic.gdx.Gdx;
import com.badlogic.gdx.assets.AssetDescriptor;
import com.badlogic.gdx.assets.AssetLoaderParameters;
import com.badlogic.gdx.assets.AssetManager;
import com.badlogic.gdx.assets.loaders.AsynchronousAssetLoader;
import com.badlogic.gdx.assets.loaders.FileHandleResolver;
import com.badlogic.gdx.files.FileHandle;
import com.badlogic.gdx.utils.Array;
import com.fteams.siftrain.entities.SimpleNotesInfo;
import com.fteams.siftrain.entities.SimpleSong;
import com.fteams.siftrain.entities.SimpleSongInfo;
import com.fteams.siftrain.entities.SongFileInfo;
import com.fteams.siftrain.util.SongUtils;
import com.google.gson.Gson;
import java.io.BufferedInputStream;
import java.io.BufferedReader;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Enumeration;
import java.util.List;
import java.util.zip.CRC32;
import java.util.zip.ZipEntry;
import java.util.zip.ZipFile;
public class SimplifiedBeatmapLoader extends AsynchronousAssetLoader<List, SimplifiedBeatmapLoader.BeatmapParameter> {
private List<SongFileInfo> beatmaps;
static int BLOCK_SIZE = 8192;
public SimplifiedBeatmapLoader(FileHandleResolver resolver) {
super(resolver);
}
@Override
public Array<AssetDescriptor> getDependencies(String fileName, FileHandle file, BeatmapParameter parameter) {
return null;
}
@Override
public void loadAsync(AssetManager manager, String fileName, FileHandle file, BeatmapParameter parameter) {
beatmaps = new ArrayList<>();
if (fileName.endsWith(".rs")) {
loadAsyncStandard(manager, fileName, file, parameter);
} else if (fileName.endsWith(".osz")) {
installOsuFiles(manager, fileName, file, parameter);
} else if (fileName.endsWith(".osu")) {
convertOsuBeatmap(manager, fileName, file, parameter);
}
}
private void loadAsyncStandard(AssetManager manager, String fileName, FileHandle file, BeatmapParameter parameter) {
FileHandle handle = resolve(fileName);
String jsonDefinition = handle.readString("UTF-8");
SongFileInfo info = new SongFileInfo();
try {
info = new Gson().fromJson(jsonDefinition, SimpleSong.class);
} catch (Exception e) {
info = new SimpleSong();
info.song_name = "Error: Invalid JSON format " + handle.file().getName();
info.difficulty = 1;
} finally {
info.setFileName(fileName);
// naming scheme for the resources is:
// File Name[difficulty]
// File Name [difficulty]
// file_name_difficulty
// this will allow the resource name to be parsed correctly and group the songs by resource
info.setResourceName(handle.nameWithoutExtension().replaceAll("(_(easy|normal|hard|expert)|(\\s?\\[.+]))$", ""));
beatmaps.add(info);
}
}
private void installOsuFiles(AssetManager manager, String fileName, FileHandle file, BeatmapParameter parameter) {
try {
Gdx.app.log("OSZ_LOADER", "installing map: " + fileName);
FileHandle handle = resolve(fileName);
ZipFile osuZipFile = new ZipFile(handle.file());
Enumeration<? extends ZipEntry> entries = osuZipFile.entries();
List<SimpleSong> songs = new ArrayList<>();
while (entries.hasMoreElements()) {
ZipEntry entry = entries.nextElement();
// get only osu files
if (entry.getName().endsWith(".osu")) {
Long crc = entry.getCrc();
try {
// check if the beatmap already exists - was extracted before
// if the osz file was found in the root directory, we go down to datafiles
FileHandle[] files;
if (handle.parent().name().equals("beatmaps")) {
files = handle.parent().child("datafiles").list(".rs");
}
// if the osz file was in the datafiles directory
else {
files = handle.parent().list(".rs");
}
boolean found = false;
// check if the entries already exist
for (FileHandle fh : files) {
if (fh.name().equals(entry.getName().replace(".osu", ".rs"))) {
Gdx.app.log("OSZ_LOADER", "Entry: [" + entry.getName().replace(".osu", ".rs") + "] exists, performing a crc check.");
SongFileInfo info = new Gson().fromJson(fh.readString(), SongFileInfo.class);
//
if (info.getCrc() != null && entry.getCrc() == crc) {
// if the CRC matches, we skip the file. Otherwise we'll re-process it.
found = true;
}
break;
}
}
if (found) {
Gdx.app.log("OSZ_LOADER", "Entry: [" + entry.getName().replace(".osu", ".rs") + "] has the same CRC, skipping.");
continue;
}
SimpleSong beatmap = processOsuStandardFile(osuZipFile.getInputStream(entry));
beatmap.setCrc(crc);
songs.add(beatmap);
storeBeatmap(beatmap, "beatmaps/datafiles/" + entry.getName().replace(".osu", ".rs"));
} catch (Exception e) {
// attempted to load a non-mania map, ignore it
Gdx.app.log("OSZ_LOADER", "Attempted to load a non-mania map: " + entry.getName());
}
}
}
for (SimpleSong song : songs) {
String songFile = song.music_file;
storeMusicFile(osuZipFile.getInputStream(osuZipFile.getEntry(songFile)), songFile);
}
// don't load files from within an osu archive, just install them
songs.clear();
} catch (IOException e) {
// something happened while loading the file
// encrypted zip, or corrupted file
// just exit
}
}
private void convertOsuBeatmap(AssetManager manager, String fileName, FileHandle file, BeatmapParameter parameter) {
try {
Gdx.app.log("OSU_LOADER", "converting map: " + fileName);
FileHandle handle = resolve(fileName);
Long crc = computeCRC(handle);
FileHandle[] files;
// if the osu file was in the root 'beatmaps' directory, go down to the datafiles directory
if (handle.parent().name().equals("beatmaps")) {
files = handle.parent().child("datafiles").list(".rs");
}
// if the osz file was in the datafiles directory
else {
files = handle.parent().list(".rs");
}
boolean found = false;
for (FileHandle fh : files) {
if (fh.name().equals(handle.name().replace(".osu", ".rs"))) {
Gdx.app.log("OSU_LOADER", "Map [" + handle.nameWithoutExtension() + ".rs] already exists, checking crc.");
SongFileInfo info = new Gson().fromJson(fh.readString(), SongFileInfo.class);
if (info.getCrc() != null && info.getCrc().equals(crc)) {
found = true;
}
break;
}
}
// don't store files which were already converted.
// if someone wants a hard reload, they can remove the .rs file
// and have the game re-generate them
if (found) {
Gdx.app.log("OSU_LOADER", "Map [" + handle.nameWithoutExtension() + ".rs] crc match, skipping.");
return;
} else {
Gdx.app.log("OSU_LOADER", "Map [" + handle.nameWithoutExtension() + ".rs] crc didn't match - generating map.");
}
InputStream is = new FileInputStream(handle.file());
SimpleSong beatmap = processOsuStandardFile(is);
beatmap.setCrc(crc);
storeBeatmap(beatmap, "beatmaps/datafiles/" + fileName.split("\\\\|/", 2)[1].replace(".osu", ".rs"));
} catch (Exception e) {
// something happened while loading the file
// just exit
Gdx.app.log("OSZ_LOADER", "Attempted to load a non-mania map: " + fileName);
}
}
private Long computeCRC(FileHandle handle) throws IOException {
File file = handle.file();
FileInputStream fis = new FileInputStream(file);
byte[] bytes = new byte[BLOCK_SIZE];
CRC32 crc32 = new CRC32();
int len = fis.read(bytes, 0, BLOCK_SIZE);
while (len != -1) {
crc32.update(bytes, 0, len);
len = fis.read(bytes, 0, BLOCK_SIZE);
}
return crc32.getValue();
}
private void storeMusicFile(InputStream inputStream, String fileName) {
File output = new File(Gdx.files.getExternalStoragePath() + "/beatmaps/soundfiles/" + fileName);
if (output.exists()) {
Gdx.app.log("OSZ_MUSIC_LOADER", "File [" + fileName + "] already exists, skipping.");
return;
}
try {
FileOutputStream fos = new FileOutputStream(output);
// tried bigger blocks, didn't really get better performance.
byte[] buffer = new byte[BLOCK_SIZE];
int len = inputStream.read(buffer, 0, BLOCK_SIZE);
while (len != -1) {
fos.write(buffer, 0, len);
len = inputStream.read(buffer, 0, BLOCK_SIZE);
}
inputStream.close();
fos.close();
} catch (IOException e) {
Gdx.app.error("OSZ_MUSIC_LOADER", "Failed to store the music file.");
}
}
private void storeBeatmap(SimpleSong beatmap, String fileName) {
File output = new File(Gdx.files.getExternalStoragePath() + "/" + fileName);
String json = new Gson().toJson(beatmap);
try {
FileOutputStream fos = new FileOutputStream(output);
fos.write(json.getBytes("UTF-8"));
fos.close();
} catch (IOException e) {
Gdx.app.error("BEATMAP_STORE", "Failed to store the beatmap file.");
}
}
private SimpleSong processOsuStandardFile(InputStream entry) throws IOException {
BufferedInputStream inputStream = new BufferedInputStream(entry);
BufferedReader reader = new BufferedReader(new InputStreamReader(inputStream));
String contents = reader.readLine();
SimpleSong song = new SimpleSong();
song.difficulty = 4;
Integer mode = 0;
SimpleSongInfo songInfo = new SimpleSongInfo();
song.song_info = new ArrayList<>();
song.song_info.add(songInfo);
songInfo.notes = new ArrayList<>();
while (contents != null) {
if (contents.startsWith("//")) {
contents = reader.readLine();
continue;
}
if (contents.startsWith("AudioFilename:")) {
song.music_file = contents.split(":", 2)[1].trim();
}
if (contents.startsWith("AudioLeadIn:")) {
song.lead_in = Integer.parseInt(contents.split(":", 2)[1].trim()) / 1000f;
}
if (contents.startsWith("Title:")) {
song.song_name = contents.split(":", 2)[1].trim();
}
// mania = mode 3
if (contents.startsWith("Mode:")) {
if (Integer.parseInt(contents.split(":", 2)[1].trim()) != 3) {
throw new RuntimeException("Invalid beatmap mode.");
}
}
if (contents.startsWith("Version:")) {
song.difficulty_name = contents.split(":", 2)[1].trim();
}
if (contents.startsWith("CircleSize:")) {
mode = Integer.parseInt(contents.split(":", 2)[1].trim());
}
if (contents.equals("[HitObjects]")) {
// convert notes into CircleMarks
contents = reader.readLine();
// format:
while (contents != null && !contents.startsWith("[")) {
if (contents.startsWith("//")) {
contents = reader.readLine();
continue;
}
// x,y,timing,flag,0,release:a:b:c:d
String[] theLine = contents.split(",", 6);
SimpleNotesInfo notesInfo = new SimpleNotesInfo();
Integer band = Integer.parseInt(theLine[0].trim()) / (512 / mode);
notesInfo.position = SongUtils.getPositionForMode(mode, band);
notesInfo.timing_sec = Integer.parseInt(theLine[2].trim()) / 1000.0;
notesInfo.effect = SongUtils.NOTE_TYPE_NORMAL;
if ((Integer.parseInt(theLine[3].trim()) & 128) != 0) {
notesInfo.effect_value = Integer.parseInt(theLine[5].trim().split(":", 2)[0].trim()) / 1000f - notesInfo.timing_sec;
notesInfo.effect = SongUtils.NOTE_TYPE_HOLD;
} else {
notesInfo.effect_value = 2.0;
}
songInfo.notes.add(notesInfo);
contents = reader.readLine();
}
}
contents = reader.readLine();
}
processEffects(song);
return song;
}
// since we're creating the maps, we need to make sure that notes which land at the same time are tagged as simultaneous
private void processEffects(SimpleSong song) {
SimpleSongInfo info = song.song_info.get(0);
List<SimpleNotesInfo> notes = info.notes;
// sort the notes by timing and position
Collections.sort(notes);
SimpleNotesInfo previousNote = notes.get(0);
for (int i = 1; i < notes.size(); i++) {
SimpleNotesInfo currentNote = notes.get(i);
// we only look for notes which start at the same time
if (currentNote.timing_sec.equals(previousNote.timing_sec)) {
previousNote.effect = previousNote.effect | SongUtils.NOTE_TYPE_SIMULT_START;
currentNote.effect = currentNote.effect | SongUtils.NOTE_TYPE_SIMULT_START;
}
previousNote = currentNote;
}
}
@Override
public List<SongFileInfo> loadSync(AssetManager manager, String fileName, FileHandle file, BeatmapParameter parameter) {
return beatmaps;
}
public class BeatmapParameter extends AssetLoaderParameters<List> {
}
}